﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using NMapper.Utils;

namespace NMapper.Mappings
{
    public class PropertyMapping<TDatabase, TDatabaseEntity, TOutput, TDbValue, TOutputValue> : IValueMapping<TDatabase, TDatabaseEntity, TDbValue, TOutput, TOutputValue>
        where TDatabase : IDisposable
    {
        private Expression<Func<MapContext<TDatabase>, TDatabaseEntity, TDbValue>> dbSelector;
        private Expression<Func<TOutput, TOutputValue>> outputSelector;
        private Func<MapContext<TDatabase>, TDatabaseEntity, TDbValue> dbSelectorFunc;
        private Func<TOutput, TOutputValue> outputSelectorFunc;
        private PropertyInfo property;
        private bool canCopyToOutput;

        public PropertyInfo TransientProperty
        {
            get { return property; }
        }

        public PropertyMapping(Expression<Func<MapContext<TDatabase>, TDatabaseEntity, TDbValue>> dbSelector, Expression<Func<TOutput, TOutputValue>> outputSelector)
        {
            this.dbSelector = dbSelector;
            this.outputSelector = outputSelector;
            dbSelectorFunc = dbSelector.Compile();
            outputSelectorFunc = outputSelector.Compile();
            canCopyToOutput = outputSelector.GetPropertyInfo() != null && outputSelector.GetPropertyInfo().GetSetMethod() != null;
        }

        LambdaExpression IDbSelector<TDatabase>.DbSelectorExpression
        {
            get { return dbSelector; }
        }

        LambdaExpression IOutputSelector.OutputSelectorExpression
        {
            get { return outputSelector; }
        }

        Func<MapContext<TDatabase>, object, object> IDbSelector<TDatabase>.DbSelector
        {
            get { return (mapContext, x) => DbSelector(mapContext, (TDatabaseEntity)x); }
        }

        public Func<MapContext<TDatabase>, TDatabaseEntity, TDbValue> DbSelector
        {
            get { return dbSelectorFunc; }
        }

        Func<object, object> IOutputSelector.OutputSelector
        {
            get { return x => OutputSelector((TOutput)x); }
        }

        public Func<TOutput, TOutputValue> OutputSelector
        {
            get { return outputSelectorFunc; }
        }

        public void DefineTransientType(ModuleBuilder module, TypeBuilder type)
        {
            if (canCopyToOutput)
            {
                var propertyName = NameHelpers.SuggestName(dbSelector, outputSelector);
                var propertyType = dbSelector.Body.Type;
                property = type.DefineProperty(propertyName, propertyType);
            }
        }

        public void DefineTransientInitialization(MapContext<TDatabase> mapContext, Type transientType, List<MemberBinding> initializers, ParameterExpression parameterExpression)
        {
            if (canCopyToOutput)
            {
                property = transientType.GetProperty(property.Name);

                var lambdaBinder = new LambdaBinder();
                var simpleDbSelector = dbSelector.PartialApplication(Expression.Constant(mapContext), null).Simplify();
                var selector = lambdaBinder.Bind(simpleDbSelector, parameterExpression);

                var body = selector.Body;
            
                var nullChecker = new LambdaNullChecker();

                // If this a nested property (Foo.Bar.Baz) and the property (on either side) is a value type, then it could be null
                // and you'll get an annoying linq to sql exception.  So instead we will always fall back to the default value of
                // the value type in this scenario.  (except for date times, for which null is illegal)
                if (nullChecker.IsNestedProperty(body) && (body.Type.IsValueType || outputSelector.Type.IsValueType) && !outputSelector.Type.IsNullableType())
                {
                    var targetType = body.Type.IsValueType ? body.Type : outputSelector.Type;
                    var defaultValue = targetType.GetDefaultValue();
                    body = nullChecker.AddNullCheckToParents(body, defaultValue);
                }

                var lhs = property;
                var rhs = body;
            
                initializers.Add(Expression.Bind(lhs.GetSetMethod(), rhs));
            }
        }

        void IMapping<TDatabase>.OnTransientInitialized(MapContext<TDatabase> mapContext, Array transient)
        {
        }

        protected virtual object GetOutputValue(MapContext<TDatabase> mapContext, object transient)
        {
            var value = property.GetValue(transient, null);
            return value;
        }

        public void MapToOutput(MapContext<TDatabase> mapContext, object input, object output)
        {
            if (canCopyToOutput)
            {
                var value = GetOutputValue(mapContext, input);
                var outputProperty = outputSelector.GetPropertyInfo();
                outputProperty.SetValue(output, value, null);                
            }
        }

        protected virtual object GetInputValue(MapContext<TDatabase> mapContext, object output)
        {
            var outputProperty = outputSelector.GetPropertyInfo();
            return outputProperty.GetValue(output, null);
        }

        public void MapToInput(MapContext<TDatabase> mapContext, object output, object input)
        {
            var value = GetInputValue(mapContext, output);
            dbSelector.SetProperty(input, value);
        }

        protected virtual string Describe()
        {
            return dbSelector.Body + " -> " + outputSelector.Body;
        }

        protected virtual string GetTypeName()
        {
            return "PropertyMapping";
        }

        public override string ToString()
        {
            return string.Format("PropertyMapping({0}, {1})", dbSelector, outputSelector);
        }
    }
}